GitHub Actionsでデプロイを並列に実行させてCI/CDを高速化してみた
開発の規模が大きくなると、CI/CDに時間がかかるようになります。特にクラウド環境を用いた開発で、インフラ構成までコードで管理している場合、差分の確認やインフラサービスの更新で処理の待ち時間が発生します。
各機能やサービスに依存関係がないのであれば、処理を並列に実行することで、デプロイ等にかかる時間を短縮することが出来ます。デプロイ以外にもビルドやテストで時間がかかっているのであれば、機能単位などに分割して並列に実行させるのも良いと思います。
本記事ではAWS環境へのデプロイをGitHub Actionsで並列に実行させてみます。
ワークフローを実装
AWS環境にデプロイするワークフローを実装します。.github/workflows
にYAMLファイルを作成すると、プッシュ時にGitHub Actionsがワークフローを実行します。
以下のワークフローでは、指定したブランチにプッシュされた際、パッケージのインストールやビルドとテスト、ブランチに対応したAWS環境への並列デプロイを実行します。
name: Deploy AWS env: PROJECT_NAME: sample on: push: branches: - develop - release - main jobs: setup: name: Setup runs-on: ubuntu-latest timeout-minutes: 5 outputs: AWS_ACCOUNT_ID: ${{ steps.setenv.outputs.AWS_ACCOUNT_ID }} STAGE_NAME: ${{ steps.setenv.outputs.STAGE_NAME }} steps: - name: Checkout uses: actions/checkout@v2 - name: Use Node uses: actions/setup-node@v1 with: node-version: "14.x" - name: Set environment id: setenv env: ITG_AWS_ACCOUNT_ID: 111111111111 STG_AWS_ACCOUNT_ID: 222222222222 PRD_AWS_ACCOUNT_ID: 333333333333 run: | if ${{ github.ref == 'refs/heads/develop' }}; then echo "::set-output name=AWS_ACCOUNT_ID::$ITG_AWS_ACCOUNT_ID" echo "::set-output name=STAGE_NAME::itg" elif ${{ github.ref == 'refs/heads/release' }}; then echo "::set-output name=AWS_ACCOUNT_ID::$STG_AWS_ACCOUNT_ID" echo "::set-output name=STAGE_NAME::stg" elif ${{ github.ref == 'refs/heads/main' }}; then echo "::set-output name=AWS_ACCOUNT_ID::$PRD_AWS_ACCOUNT_ID" echo "::set-output name=STAGE_NAME::prd" else echo "Invalid branch name." exit 1 fi - name: Cache node modules uses: actions/cache@v2 env: cache-name: cache-node-modules with: path: "**/node_modules" key: ${{ runner.os }}-${{ env.cache-name }}-${{ hashFiles('**/yarn.lock') }} - name: Cache build files uses: actions/cache@v2 env: cache-name: cache-build-files with: path: "**/build" key: ${{ runner.os }}-${{ env.cache-name }}-${{ github.sha }} - name: Install run: make install - name: Build run: make build test: name: Test needs: - setup runs-on: ubuntu-latest timeout-minutes: 5 steps: - name: Checkout uses: actions/checkout@v2 - name: Use Node uses: actions/setup-node@v1 with: node-version: "14.x" - name: Restore node modules uses: actions/cache@v2 env: cache-name: cache-node-modules with: path: "**/node_modules" key: ${{ runner.os }}-${{ env.cache-name }}-${{ hashFiles('**/yarn.lock') }} - name: Lint run: make lint - name: Unit test run: make test-unit deploy-1: name: Deploy 1 needs: - setup - test runs-on: ubuntu-latest timeout-minutes: 10 env: AWS_DEFAULT_REGION: ap-northeast-1 AWS_ACCOUNT_ID: ${{ needs.setup.outputs.AWS_ACCOUNT_ID }} STAGE_NAME: ${{ needs.setup.outputs.STAGE_NAME }} steps: - name: Checkout uses: actions/checkout@v2 - name: Use Node uses: actions/setup-node@v1 with: node-version: "14.x" - name: Configure AWS Credentials uses: aws-actions/configure-aws-credentials@v1 with: aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }} aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} aws-region: ${{ env.AWS_DEFAULT_REGION }} role-to-assume: arn:aws:iam::${{ env.AWS_ACCOUNT_ID }}:role/${{ env.STAGE_NAME }}-${{ env.PROJECT_NAME }}-assume-role role-duration-seconds: 3600 - name: Restore node modules uses: actions/cache@v2 env: cache-name: cache-node-modules with: path: "**/node_modules" key: ${{ runner.os }}-${{ env.cache-name }}-${{ hashFiles('**/yarn.lock') }} - name: Restore build files uses: actions/cache@v2 env: cache-name: cache-build-files with: path: "**/build" key: ${{ runner.os }}-${{ env.cache-name }}-${{ github.sha }} - name: Deploy run: | make deploy TARGET=stack-name-1 STAGE=$STAGE_NAME deploy-2: name: Deploy 2 needs: - setup - test runs-on: ubuntu-latest timeout-minutes: 10 env: AWS_DEFAULT_REGION: ap-northeast-1 AWS_ACCOUNT_ID: ${{ needs.setup.outputs.AWS_ACCOUNT_ID }} STAGE_NAME: ${{ needs.setup.outputs.STAGE_NAME }} steps: - name: Checkout uses: actions/checkout@v2 - name: Use Node uses: actions/setup-node@v1 with: node-version: "14.x" - name: Configure AWS Credentials uses: aws-actions/configure-aws-credentials@v1 with: aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }} aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} aws-region: ${{ env.AWS_DEFAULT_REGION }} role-to-assume: arn:aws:iam::${{ env.AWS_ACCOUNT_ID }}:role/${{ env.STAGE_NAME }}-${{ env.PROJECT_NAME }}-assume-role role-duration-seconds: 3600 - name: Restore node modules uses: actions/cache@v2 env: cache-name: cache-node-modules with: path: "**/node_modules" key: ${{ runner.os }}-${{ env.cache-name }}-${{ hashFiles('**/yarn.lock') }} - name: Restore build files uses: actions/cache@v2 env: cache-name: cache-build-files with: path: "**/build" key: ${{ runner.os }}-${{ env.cache-name }}-${{ github.sha }} - name: Deploy run: | make deploy TARGET=stack-name-2 STAGE=$STAGE_NAME
解説
ワークフローの実装について、要点を解説していきます。
name: Deploy AWS env: PROJECT_NAME: sample on: push: branches: - develop - release - main
ワークフロー名とワークフローの環境変数を定義します。on
push
branches
で指定したブランチにプッシュした際にワークフローが実行されるように設定しています。
setup: name: Setup runs-on: ubuntu-latest timeout-minutes: 5 outputs: AWS_ACCOUNT_ID: ${{ steps.setenv.outputs.AWS_ACCOUNT_ID }} STAGE_NAME: ${{ steps.setenv.outputs.STAGE_NAME }}
Setupジョブの設定を定義しています。コードの記述ミスなどで長時間実行されないように、必ずtimeout-minutes
を設定しておきます。他のジョブで値を取り込めるようにoutputs
を定義します。(後ほど登場します。)
- name: Set environment id: setenv env: ITG_AWS_ACCOUNT_ID: 111111111111 STG_AWS_ACCOUNT_ID: 222222222222 PRD_AWS_ACCOUNT_ID: 333333333333 run: | if ${{ github.ref == 'refs/heads/develop' }}; then echo "::set-output name=AWS_ACCOUNT_ID::$ITG_AWS_ACCOUNT_ID" echo "::set-output name=STAGE_NAME::itg" elif ${{ github.ref == 'refs/heads/release' }}; then echo "::set-output name=AWS_ACCOUNT_ID::$STG_AWS_ACCOUNT_ID" echo "::set-output name=STAGE_NAME::stg" elif ${{ github.ref == 'refs/heads/main' }}; then echo "::set-output name=AWS_ACCOUNT_ID::$PRD_AWS_ACCOUNT_ID" echo "::set-output name=STAGE_NAME::prd" else echo "Invalid branch name." exit 1 fi
プッシュされたブランチに対してデプロイ先のAWSアカウントを設定しています。他のジョブでも設定した値を利用できるように、set-output
を使用します。
- name: Cache node modules uses: actions/cache@v2 env: cache-name: cache-node-modules with: path: "**/node_modules" key: ${{ runner.os }}-${{ env.cache-name }}-${{ hashFiles('**/yarn.lock') }} - name: Cache build files uses: actions/cache@v2 env: cache-name: cache-build-files with: path: "**/build" key: ${{ runner.os }}-${{ env.cache-name }}-${{ github.sha }} - name: Install run: make install - name: Build run: make build
パッケージとビルドしたファイルを他のジョブで利用できるようにキャッシュしています。
test: name: Test needs: - setup runs-on: ubuntu-latest timeout-minutes: 5
Testジョブの設定を定義しています。needs
でsetup
を指定しているので、Setupジョブが完了後に実行されます。
- name: Restore node modules uses: actions/cache@v2 env: cache-name: cache-node-modules with: path: "**/node_modules" key: ${{ runner.os }}-${{ env.cache-name }}-${{ hashFiles('**/yarn.lock') }} - name: Lint run: make lint - name: Unit test run: make test-unit
Setupジョブでキャッシュしたnode_modules
を取得して、テストを実行しています。
deploy-1: name: Deploy 1 needs: - setup - test runs-on: ubuntu-latest timeout-minutes: 10
Deployジョブの設定を定義しています。needs
でsetup
とtest
を指定しているので、2つのジョブが完了後に実行されます。他のDeployジョブでも同様にneeds
を設定することで、ジョブを並列に実行することができます。
env: AWS_DEFAULT_REGION: ap-northeast-1 AWS_ACCOUNT_ID: ${{ needs.setup.outputs.AWS_ACCOUNT_ID }} STAGE_NAME: ${{ needs.setup.outputs.STAGE_NAME }}
ジョブの環境変数を定義しています。Setupジョブで出力したAWS_ACCOUNT_ID
とSTAGE_NAME
を環境変数にセットします。
- name: Configure AWS Credentials uses: aws-actions/configure-aws-credentials@v1 with: aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }} aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} aws-region: ${{ env.AWS_DEFAULT_REGION }} role-to-assume: arn:aws:iam::${{ env.AWS_ACCOUNT_ID }}:role/${{ env.STAGE_NAME }}-${{ env.PROJECT_NAME }}-assume-role role-duration-seconds: 3600
指定したAWSアカウントのIAMロールにAssume Roleしています。AWS_ACCESS_KEY_ID
とAWS_SECRET_ACCESS_KEY
はあらかじめOrganizationもしくはリポジトリのSecretsに登録しておきます。また、Assume Role先のIAM Roleも作成しておく必要があります。これにより、AWS CLIやCDKでAWS環境へデプロイを実行できるようになります。
- name: Restore node modules uses: actions/cache@v2 env: cache-name: cache-node-modules with: path: "**/node_modules" key: ${{ runner.os }}-${{ env.cache-name }}-${{ hashFiles('**/yarn.lock') }} - name: Restore build files uses: actions/cache@v2 env: cache-name: cache-build-files with: path: "**/build" key: ${{ runner.os }}-${{ env.cache-name }}-${{ github.sha }} - name: Deploy run: | make deploy TARGET=stack-name-1 STAGE=$STAGE_NAME
最後にSetupジョブでキャッシュしたパッケージとビルドファイルを取得して、デプロイを実行しています。デプロイのジョブを並列で実行するために、CloudFormationのスタックはある程度分けています。なお、make deploy
コマンドではAWS CDKのdeployコマンドなどを実行しています。
ジョブ名はスタック名などにしておくと分かりやすいかもしれません。
まとめ
GitHub Actionsのワークフローではneeds
によってジョブの依存関係を定義して、並列実行を分かりやすく実装することが出来ました。また、キャッシュやAWSの資格情報設定などのActionが提供されていて、ワークフローの定義がとても簡単でした。
GitHub Actionsのワークフロー構文やコンテキストについてはドキュメントに分かりやすく記載されているので、一度目を通してから実装に着手するのが良いと思います。